package edu.northwestern.cbits.purple_robot_manager.plugins; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.mutable.MutableInt; import org.json.JSONArray; import org.json.JSONException; import android.annotation.SuppressLint; import android.app.ActivityManager.RunningTaskInfo; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.location.Location; import android.net.wifi.ScanResult; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; @SuppressLint("NewApi") public class StreamingJacksonUploadPlugin extends DataUploadPlugin { private final static String ERROR_FILE_EXTENSION = ".error"; private final static String NORMAL_FILE_EXTENSION = ".jackson"; private static final String NORMAL_TEMP_FILE_EXTENSION = ".jackson-temp"; private static final String PRIORITY_FILE_EXTENSION = ".priority"; private static final String PRIORITY_TEMP_FILE_EXTENSION = ".priority-temp"; private static final String ON_DEMAND_FILE_EXTENSION = ".ondemand"; private static final String ON_DEMAND_TEMP_FILE_EXTENSION = ".ondemand-temp"; public static final String ENABLED = "config_enable_streaming_jackson_data_server"; private static final String UPLOAD_SIZE = "config_streaming_jackson_upload_size"; private static final String UPLOAD_INTERVAL = "config_streaming_jackson_upload_interval"; private static final String UPLOAD_SIZE_DEFAULT = "262114"; private static final String UPLOAD_INTERVAL_DEFAULT = "300"; public static final boolean ENABLED_DEFAULT = false; public static final String LAST_UPLOAD_TIME = "streaming_json_last_upload"; public static final String LAST_UPLOAD_SIZE = "streaming_json_last_upload_size"; private static final String ON_DEMAND_DURATION = "config_streaming_jackson_buffer_duration"; private static final String ON_DEMAND_DURATION_DEFAULT = "900000"; private JsonGenerator _normalGenerator = null; private JsonGenerator _priorityGenerator = null; private JsonGenerator _onDemandGenerator = null; private long _lastAttempt = 0; private File _currentNormalFile = null; private File _currentPriorityFile = null; private File _currentOnDemandFile = null; private ArrayList<String> _regularFilenames = new ArrayList<>(); private ArrayList<String> _priorityFilenames = new ArrayList<>(); public String[] respondsTo() { String[] activeActions = { Probe.PROBE_READING, OutputPlugin.FORCE_UPLOAD, Probe.PROBE_TRANSMIT_BUFFER }; return activeActions; } private void uploadFiles(final Context context, final SharedPreferences prefs, final int callLevel) { long now = System.currentTimeMillis(); long duration = Long.parseLong(prefs.getString(StreamingJacksonUploadPlugin.UPLOAD_INTERVAL, StreamingJacksonUploadPlugin.UPLOAD_INTERVAL_DEFAULT)) * 1000; if (now - this._lastAttempt < duration || this.shouldAttemptUpload(context) == false) return; this._lastAttempt = now; final StreamingJacksonUploadPlugin me = this; Runnable r = new Runnable() { @SuppressLint("TrulyRandom") public void run() { synchronized (me) { try { File pendingFolder = me.getPendingFolder(); me.closeOpenSession(context, me._priorityGenerator, Probe.PROBE_TRANSMIT_MODE_PRIORITY); me._currentPriorityFile = null; me._priorityGenerator = null; me.closeOpenSession(context, me._normalGenerator, Probe.PROBE_TRANSMIT_MODE_NORMAL); me._currentNormalFile = null; me._normalGenerator = null; me.closeOpenSession(context, me._onDemandGenerator, Probe.PROBE_TRANSMIT_MODE_ON_DEMAND); me._currentOnDemandFile = null; me._onDemandGenerator = null; String[] filenames = {}; final MutableInt found = new MutableInt(0); String[] priorityFilenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { // Only return first 256 for performance reasons... if (found.intValue() >= 256) return false; if (filename.endsWith(StreamingJacksonUploadPlugin.PRIORITY_FILE_EXTENSION)) { found.add(1); return true; } return false; } }); filenames = priorityFilenames; if (filenames == null || found.intValue() == 0) { if (me._regularFilenames.size() == 0) { String[] regularFilenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { // Only return first 1024 for performance reasons... if (found.intValue() >= 1024) return false; if (filename.endsWith(StreamingJacksonUploadPlugin.NORMAL_FILE_EXTENSION)) { found.add(1); return true; } return false; } }); if (regularFilenames != null) { for (String filename : regularFilenames) me._regularFilenames.add(filename); } } filenames = me._regularFilenames.toArray(new String[0]); } if (filenames.length < 1) return; SecureRandom random = new SecureRandom(); int index = 0; if (filenames.length > 1) index = random.nextInt(filenames.length); String filename = filenames[index]; me._priorityFilenames.remove(filename); me._regularFilenames.remove(filename); File payloadFile = new File(pendingFolder, filename); String payload = FileUtils.readFileToString(payloadFile, "UTF-8"); int result = me.transmitPayload(prefs, payload); if (result == DataUploadPlugin.RESULT_SUCCESS) { SharedPreferences.Editor e = prefs.edit(); e.putLong(StreamingJacksonUploadPlugin.LAST_UPLOAD_TIME, System.currentTimeMillis()); e.putLong(StreamingJacksonUploadPlugin.LAST_UPLOAD_SIZE, payloadFile.length()); e.commit(); payloadFile.delete(); me._lastAttempt = 0; me.uploadFiles(context, prefs, callLevel + 1); } else if (result == DataUploadPlugin.RESULT_NO_CONNECTION) { // WiFi only or no connection... } else if (result == DataUploadPlugin.RESULT_NO_POWER) { // Device isn't charging... } else { try { JSONArray jsonPayload = new JSONArray(payload); // JSON is valid } catch (JSONException e) { // Invalid JSON, log results. LogManager.getInstance(context).logException(e); HashMap<String, Object> details = new HashMap<>(); payloadFile.renameTo(new File(payloadFile.getAbsolutePath() + StreamingJacksonUploadPlugin.ERROR_FILE_EXTENSION)); details.put("name", payloadFile.getAbsolutePath()); details.put("size", payloadFile.length()); LogManager.getInstance(context).log("corrupted_file", details); } } } catch (IOException e) { LogManager.getInstance(context).logException(e); me.broadcastMessage(context.getString(R.string.message_general_error, e.getMessage()), true); } } } }; Thread t = new Thread(r); t.start(); } public void processIntent(final Intent intent) { final Context context = this.getContext().getApplicationContext(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.getBoolean(StreamingJacksonUploadPlugin.ENABLED, StreamingJacksonUploadPlugin.ENABLED_DEFAULT) == false) return; synchronized (this) { String action = intent.getAction(); if (OutputPlugin.FORCE_UPLOAD.equals(action)) { this._lastAttempt = 0; this.uploadFiles(context, prefs, 0); } else if (Probe.PROBE_READING.equals(action)) { Bundle extras = intent.getExtras(); if (extras.containsKey(DataUploadPlugin.TRANSMIT_KEY) && extras.getBoolean(DataUploadPlugin.TRANSMIT_KEY) == false) { } else { int transmitMode = extras.getInt(Probe.PROBE_TRANSMIT_MODE, Probe.PROBE_TRANSMIT_MODE_NORMAL); if (extras.containsKey("PRIORITY") && extras.getBoolean("PRIORITY")) transmitMode = Probe.PROBE_TRANSMIT_MODE_PRIORITY; JsonGenerator generator = this._normalGenerator; File currentFile = this._currentNormalFile; if (transmitMode == Probe.PROBE_TRANSMIT_MODE_PRIORITY) { generator = this._priorityGenerator; currentFile = this._currentPriorityFile; } else if (transmitMode == Probe.PROBE_TRANSMIT_MODE_ON_DEMAND) { generator = this._onDemandGenerator; currentFile = this._currentOnDemandFile; } extras.remove(Probe.PROBE_TRANSMIT_MODE); long now = System.currentTimeMillis(); try { File pendingFolder = this.getPendingFolder(); if (currentFile != null) { File f = currentFile.getAbsoluteFile(); long length = f.length(); long modDelta = now - f.lastModified(); long size = Long.parseLong(prefs.getString(StreamingJacksonUploadPlugin.UPLOAD_SIZE, StreamingJacksonUploadPlugin.UPLOAD_SIZE_DEFAULT)); if (generator != null && (length > size || modDelta > 60000)) { this.closeOpenSession(context, generator, transmitMode); generator = null; } } this.uploadFiles(context, prefs, 0); if (generator == null) { JsonFactory factory = new JsonFactory(); if (transmitMode == Probe.PROBE_TRANSMIT_MODE_NORMAL) { this._currentNormalFile = new File(pendingFolder, now + StreamingJacksonUploadPlugin.NORMAL_TEMP_FILE_EXTENSION); this._normalGenerator = factory.createGenerator(this._currentNormalFile, JsonEncoding.UTF8); generator = this._normalGenerator; } else if (transmitMode == Probe.PROBE_TRANSMIT_MODE_PRIORITY) { this._currentPriorityFile = new File(pendingFolder, now + StreamingJacksonUploadPlugin.PRIORITY_TEMP_FILE_EXTENSION); this._priorityGenerator = factory.createGenerator(this._currentPriorityFile, JsonEncoding.UTF8); generator = this._priorityGenerator; } else if (transmitMode == Probe.PROBE_TRANSMIT_MODE_ON_DEMAND) { this._currentOnDemandFile = new File(pendingFolder, now + StreamingJacksonUploadPlugin.ON_DEMAND_TEMP_FILE_EXTENSION); this._onDemandGenerator = factory.createGenerator(this._currentOnDemandFile, JsonEncoding.UTF8); generator = this._onDemandGenerator; } generator.writeStartArray(); } StreamingJacksonUploadPlugin.writeBundle(this.getContext(), generator, extras); generator.flush(); } catch (IOException e) { LogManager.getInstance(this.getContext()).logException(e); } } } else if (Probe.PROBE_TRANSMIT_BUFFER.equals(action)) { FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.endsWith(StreamingJacksonUploadPlugin.ON_DEMAND_FILE_EXTENSION); } }; final File pendingFolder = this.getPendingFolder(); for (String filename : pendingFolder.list(filter)) { String finalFile = filename.replace(StreamingJacksonUploadPlugin.ON_DEMAND_FILE_EXTENSION, StreamingJacksonUploadPlugin.NORMAL_FILE_EXTENSION); try { FileUtils.moveFile(new File(pendingFolder, filename), new File(pendingFolder, finalFile)); } catch (IOException e) { LogManager.getInstance(this.getContext()).logException(e); } } } } } private void closeOpenSession(Context context, JsonGenerator generator, int transmitMode) throws IOException { String extension = StreamingJacksonUploadPlugin.NORMAL_FILE_EXTENSION; String tempExtension = StreamingJacksonUploadPlugin.NORMAL_TEMP_FILE_EXTENSION; File tempFile = this._currentNormalFile; if (transmitMode == Probe.PROBE_TRANSMIT_MODE_PRIORITY) { extension = StreamingJacksonUploadPlugin.PRIORITY_FILE_EXTENSION; tempExtension = StreamingJacksonUploadPlugin.PRIORITY_TEMP_FILE_EXTENSION; tempFile = this._currentPriorityFile; } else if (transmitMode == Probe.PROBE_TRANSMIT_MODE_ON_DEMAND) { extension = StreamingJacksonUploadPlugin.ON_DEMAND_FILE_EXTENSION; tempExtension = StreamingJacksonUploadPlugin.ON_DEMAND_TEMP_FILE_EXTENSION; tempFile = this._currentOnDemandFile; } if (generator == null || tempFile == null) return; final File pendingFolder = this.getPendingFolder(); generator.writeEndArray(); generator.flush(); generator.close(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final long onDemandDuration = Long.parseLong(prefs.getString(StreamingJacksonUploadPlugin.ON_DEMAND_DURATION, StreamingJacksonUploadPlugin.ON_DEMAND_DURATION_DEFAULT)); String finalFile = tempFile.getAbsolutePath().replace(tempExtension, extension); if (transmitMode == Probe.PROBE_TRANSMIT_MODE_NORMAL) { this._currentNormalFile = null; this._normalGenerator = null; } else if (transmitMode == Probe.PROBE_TRANSMIT_MODE_PRIORITY) { this._currentPriorityFile = null; this._priorityGenerator = null; } else if (transmitMode == Probe.PROBE_TRANSMIT_MODE_ON_DEMAND) { this._currentOnDemandFile = null; this._onDemandGenerator = null; } Log.e("PR", System.currentTimeMillis() + " -- MOVE FILE: " + tempFile.getName() + " (" + tempFile.length() + ") -> " + finalFile); FileUtils.moveFile(tempFile, new File(finalFile)); final String finalTempExtension = tempExtension; String[] filenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { return filename.endsWith(finalTempExtension); } }); if (filenames == null) filenames = new String[0]; for (String filename : filenames) { File toDelete = new File(pendingFolder, filename); toDelete.delete(); } if (transmitMode == Probe.PROBE_TRANSMIT_MODE_ON_DEMAND) { Runnable r = new Runnable() { @Override public void run() { long now = System.currentTimeMillis(); FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File dir, String filename) { if (filename.endsWith(StreamingJacksonUploadPlugin.ON_DEMAND_FILE_EXTENSION)) return true; return false; } }; for (String filename : pendingFolder.list(filter)) { String[] tokens = filename.split("\\."); long created = Long.parseLong(tokens[0]); if ((now - created) > onDemandDuration) { File expiredFile = new File(pendingFolder, filename); expiredFile.delete(); } } } }; Thread t = new Thread(r); t.start(); } } @SuppressWarnings("unchecked") public static void writeBundle(Context context, JsonGenerator generator, Bundle bundle) { try { generator.writeStartObject(); Map<String, Object> values = OutputPlugin.getValues(bundle); for (String key : values.keySet()) { Object value = values.get(key); if (value == null || key == null) { // Skip } else { if (value instanceof String) generator.writeStringField(key, (String) value); else if (value instanceof float[]) { float[] floats = (float[]) value; generator.writeArrayFieldStart(key); for (float f : floats) generator.writeNumber(f); generator.writeEndArray(); } else if (value instanceof int[]) { int[] ints = (int[]) value; generator.writeArrayFieldStart(key); for (int i : ints) generator.writeNumber(i); generator.writeEndArray(); } else if (value instanceof long[]) { long[] longs = (long[]) value; generator.writeArrayFieldStart(key); for (long l : longs) generator.writeNumber(l); generator.writeEndArray(); } else if (value instanceof double[]) { double[] doubles = (double[]) value; generator.writeArrayFieldStart(key); for (double d : doubles) generator.writeNumber(d); generator.writeEndArray(); } else if (value instanceof Float) { Float f = (Float) value; generator.writeNumberField(key, f); } else if (value instanceof Integer) { Integer i = (Integer) value; generator.writeNumberField(key, i); } else if (value instanceof Long) { Long l = (Long) value; generator.writeNumberField(key, l); } else if (value instanceof Boolean) { Boolean b = (Boolean) value; generator.writeBooleanField(key, b); } else if (value instanceof Short) { Short s = (Short) value; generator.writeNumberField(key, s); } else if (value instanceof Double) { Double d = (Double) value; if (d.isInfinite()) generator.writeNumberField(key, Double.MAX_VALUE); else generator.writeNumberField(key, d); } else if (value instanceof List) { List<Object> list = (List<Object>) value; generator.writeArrayFieldStart(key); for (Object o : list) { if (o instanceof String) generator.writeString(o.toString()); else if (o instanceof Bundle) StreamingJacksonUploadPlugin.writeBundle(context, generator, (Bundle) o); else if (o instanceof ScanResult) { ScanResult s = (ScanResult) o; generator.writeStartObject(); if (s.BSSID != null) generator.writeStringField("BSSID", s.BSSID); if (s.SSID != null) generator.writeStringField("SSID", s.SSID); if (s.capabilities != null) generator.writeStringField("Capabilities", s.capabilities); generator.writeNumberField("Frequency", s.frequency); generator.writeNumberField("Level dBm", s.level); generator.writeEndObject(); } else if (o instanceof RunningTaskInfo) { RunningTaskInfo r = (RunningTaskInfo) o; generator.writeStartObject(); if (r.baseActivity != null) generator.writeStringField("Base Activity", r.baseActivity.getPackageName()); if (r.description != null) generator.writeStringField("Description", r.description.toString()); generator.writeNumberField("Activity Count", r.numActivities); generator.writeNumberField("Running Activity Count", r.numRunning); generator.writeEndObject(); } else if (o instanceof ApplicationInfo) { ApplicationInfo a = (ApplicationInfo) o; generator.writeString(a.packageName); } else if (o instanceof Location) { Location l = (Location) o; generator.writeStartObject(); generator.writeNumberField("Accuracy", l.getAccuracy()); generator.writeNumberField("Altitude", l.getAltitude()); generator.writeNumberField("Bearing", l.getBearing()); generator.writeNumberField("Latitude", l.getLatitude()); generator.writeNumberField("Longitude", l.getLongitude()); generator.writeNumberField("Speed", l.getSpeed()); generator.writeNumberField("Timestamp", l.getTime()); if (l.getProvider() != null) generator.writeStringField("Provider", l.getProvider()); else generator.writeStringField("Provider", "Unknown"); generator.writeEndObject(); } else Log.e("PR", "LIST OBJ: " + o.getClass().getCanonicalName() + " IN " + key); } generator.writeEndArray(); } else if (value instanceof Location) { Location l = (Location) value; generator.writeStartObject(); generator.writeNumberField("Accuracy", l.getAccuracy()); generator.writeNumberField("Altitude", l.getAltitude()); generator.writeNumberField("Bearing", l.getBearing()); generator.writeNumberField("Latitude", l.getLatitude()); generator.writeNumberField("Longitude", l.getLongitude()); generator.writeNumberField("Speed", l.getSpeed()); generator.writeNumberField("Timestamp", l.getTime()); if (l.getProvider() != null) generator.writeStringField("Provider", l.getProvider()); else generator.writeStringField("Provider", "Unknown"); generator.writeEndObject(); } else if (value instanceof BluetoothClass) { BluetoothClass btClass = (BluetoothClass) value; generator.writeStringField(key, btClass.toString()); } else if (value instanceof BluetoothDevice) { BluetoothDevice device = (BluetoothDevice) value; generator.writeStartObject(); if (device.getBondState() == BluetoothDevice.BOND_BONDED) generator.writeStringField("Bond State", "Bonded"); else if (device.getBondState() == BluetoothDevice.BOND_BONDING) generator.writeStringField("Bond State", "Bonding"); else generator.writeStringField("Bond State", "None"); generator.writeStringField("Device Address", device.getAddress()); generator.writeStringField("Device Class", device.getBluetoothClass().toString()); generator.writeEndObject(); } else if (value instanceof Bundle) { generator.writeFieldName(key); StreamingJacksonUploadPlugin.writeBundle(context, generator, (Bundle) value); } else Log.e("PR", "GOT TYPE " + value.getClass().getCanonicalName() + " FOR " + key); } } generator.writeEndObject(); } catch (IOException e) { LogManager.getInstance(context).logException(e); } } public int pendingFilesCount() { File pendingFolder = this.getPendingFolder(); String[] filenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { if (filename.endsWith(StreamingJacksonUploadPlugin.NORMAL_FILE_EXTENSION)) return true; else if (filename.endsWith(StreamingJacksonUploadPlugin.NORMAL_TEMP_FILE_EXTENSION)) return true; else if (filename.endsWith(StreamingJacksonUploadPlugin.PRIORITY_FILE_EXTENSION)) return true; else if (filename.endsWith(StreamingJacksonUploadPlugin.PRIORITY_TEMP_FILE_EXTENSION)) return true; if (filename.endsWith(StreamingJacksonUploadPlugin.ON_DEMAND_FILE_EXTENSION)) return true; else if (filename.endsWith(StreamingJacksonUploadPlugin.ON_DEMAND_TEMP_FILE_EXTENSION)) return true; return false; } }); if (filenames == null) filenames = new String[0]; return filenames.length; } public long pendingFilesSize() { File pendingFolder = this.getPendingFolder(); String[] filenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { if (filename.endsWith(StreamingJacksonUploadPlugin.NORMAL_FILE_EXTENSION)) return true; else if (filename.endsWith(StreamingJacksonUploadPlugin.NORMAL_TEMP_FILE_EXTENSION)) return true; else if (filename.endsWith(StreamingJacksonUploadPlugin.PRIORITY_FILE_EXTENSION)) return true; else if (filename.endsWith(StreamingJacksonUploadPlugin.PRIORITY_TEMP_FILE_EXTENSION)) return true; if (filename.endsWith(StreamingJacksonUploadPlugin.ON_DEMAND_FILE_EXTENSION)) return true; else if (filename.endsWith(StreamingJacksonUploadPlugin.ON_DEMAND_TEMP_FILE_EXTENSION)) return true; return false; } }); if (filenames == null) filenames = new String[0]; if (filenames.length < 1024) { try { return FileUtils.sizeOf(pendingFolder); } catch (IllegalArgumentException e) { // File went away - try again... return this.pendingFilesSize(); } } return 2L * 1024 * 1024 * 1024; } public boolean isEnabled(Context context) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); return prefs.getBoolean(StreamingJacksonUploadPlugin.ENABLED, StreamingJacksonUploadPlugin.ENABLED_DEFAULT); } }